﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

/* TODO:
 * - generate different characters when a character set is repeated, ex. [ab]+ or (a|b)+
 * - always include optional group when it contains a named capture which exists in the dict, 
 *     ex. `/users(?:/(?:<id>\d+))?` when [id] exists in the dict, the optional group should always be included
 * - support for regex options, including inline options, ex. (?x)|
 */

namespace RegexTools
{
    public class Revex
    {
        private static readonly Random _R = new Random();

        public int MaxQuantity = 10;

        public IList<char> AllCharacters = Enumerable.Range(0, 256).Select(Convert.ToChar).Where(c => !char.IsControl(c)).ToArray();
        public IList<char> WordCharacters = Enumerable.Range(0, 256).Select(Convert.ToChar).Where(c => Regex.IsMatch(c.ToString(), @"\w")).ToArray();
        public IList<char> NonWordCharacters = Enumerable.Range(0, 256).Select(Convert.ToChar).Where(c => Regex.IsMatch(c.ToString(), @"\W")).ToArray();
        public IList<char> SpaceCharacters = Enumerable.Range(0, 256).Select(Convert.ToChar).Where(c => Regex.IsMatch(c.ToString(), @"\s")).ToArray();
        public IList<char> NonSpaceCharacters = Enumerable.Range(0, 256).Select(Convert.ToChar).Where(c => Regex.IsMatch(c.ToString(), @"\S")).ToArray();
        public IList<char> DigitCharacters = Enumerable.Range(0, 256).Select(Convert.ToChar).Where(c => Regex.IsMatch(c.ToString(), @"\d")).ToArray();
        public IList<char> NonDigitCharacters = Enumerable.Range(0, 256).Select(Convert.ToChar).Where(c => Regex.IsMatch(c.ToString(), @"\D")).ToArray();
        public IList<char> WildCharacters = Enumerable.Range(0, 256).Select(Convert.ToChar).Where(c => Regex.IsMatch(c.ToString(), @".")).ToArray();

        public Func<IList<char>, char> CharacterClassSelector = list => list[_R.Next(list.Count)];
        public Func<IList<List<string>>, List<string>> DisjunctSelector = list => list[_R.Next(list.Count)];

        public string Reverse(Regex regex, IList<object> args = null, IDictionary<string, object> kwargs = null)
        {
            return Reverse(regex.ToString(), args, kwargs);
        }

        public string Reverse(string pattern, IList<object> args = null, IDictionary<string, object> kwargs = null)
        {
            const string specialChars = @"^$.+*?|\([{";
            Func<string, string> recurse = null;
            int argc = 0;

            if (args == null) args = new List<object>();
            if (kwargs == null) kwargs = new Dictionary<string, object>();

            recurse = pat =>
            {
                var tokens = new List<string>();
                var disjuncts = new List<List<string>>();

                for (int i = 0; i < pat.Length; ++i)
                {
                    if (!specialChars.Contains(pat[i]) || pat.IsEscaped(i))
                    {
                        tokens.Add(pat[i].ToString());
                    }
                    else if (pat[i] == '\\')
                    {
                        var match = Regex.Match(pat.Substring(i + 1),
                            @"^([abtrvfnewWsSdDAZzGbB\\]|[0-9]{1,3}|x[0-9a-fA-F]{2}|c[a-zA-Z]|u[0-9a-fA-F]{4}|[pP]\{\w+\}|k<\w+>)");
                        if (match.Success)
                        {
                            switch (match.Value[0])
                            {
                                case 'w':
                                    tokens.Add(CharacterClassSelector(WordCharacters).ToString());
                                    break;
                                case 'W':
                                    tokens.Add(CharacterClassSelector(NonWordCharacters).ToString());
                                    break;
                                case 's':
                                    tokens.Add(CharacterClassSelector(SpaceCharacters).ToString());
                                    break;
                                case 'S':
                                    tokens.Add(CharacterClassSelector(NonSpaceCharacters).ToString());
                                    break;
                                case 'd':
                                    tokens.Add(CharacterClassSelector(DigitCharacters).ToString());
                                    break;
                                case 'D':
                                    tokens.Add(CharacterClassSelector(NonDigitCharacters).ToString());
                                    break;
                                case 'k':
                                    {
                                        string key = match.Value.Slice(2, -1);
                                        tokens.Add(kwargs[key].ToString());
                                    }
                                    break;
                                case 'p':
                                case 'P':
                                    throw new NotSupportedException("Unicode categories are not supported.");
                                case 'A': // the start of the string
                                case 'Z': // the end of the string or before \n at the end of the string
                                case 'z': // the end of the string
                                case 'G': // the point where the previous match ended
                                case 'b': // word boundary
                                case 'B': // non-word boundary
                                    break;
                                default:
                                    {
                                        int argIndex;
                                        if (int.TryParse(match.Value, out argIndex) && --argIndex >= 0)
                                        {
                                            if (argIndex < args.Count)
                                                tokens.Add(args[argIndex].ToString());
                                            else
                                            {
                                                int kwIndex = argIndex - args.Count;
                                                tokens.Add(kwIndex < kwargs.Count
                                                    ? kwargs.ElementAt(kwIndex).Value.ToString()
                                                    : Convert.ToChar(Convert.ToInt32(match.Value, 8)).ToString());
                                            }
                                        }
                                        else tokens.Add(Regex.Unescape("\\" + match.Value));
                                    }
                                    break;
                            }

                            i += match.Value.Length;
                        }
                    }
                    else if (pat[i] == '.')
                    {
                        tokens.Add(CharacterClassSelector(WildCharacters).ToString());
                    }
                    else if (pat[i] == '$')
                    {
                        break;
                    }
                    else if (pat[i] == '(')
                    {
                        var sb = new StringBuilder();
                        int depth = 1;
                        string group;
                        Match match;

                        while (++i < pat.Length)
                        {
                            if (pat[i] == '(' && !pat.IsEscaped(i)) ++depth;
                            else if (pat[i] == ')' && !pat.IsEscaped(i) && --depth == 0) break;
                            sb.Append(pat[i]);
                        }

                        group = sb.ToString();

                        if ((match = Regex.Match(group, @"^\?<(\w+)>")).Success) // named capture
                        {
                            string groupName = match.Groups[1].Value;
                            if (kwargs.ContainsKey(groupName))
                            {
                                tokens.Add(kwargs[groupName].ToString());
                            }
                            else
                            {
                                string value = recurse(group.Substring(match.Length));
                                kwargs[groupName] = value;
                                tokens.Add(value);
                            }
                        }
                        else if ((match = Regex.Match(group, @"^\?(<?=|:|>)")).Success) // positive look-ahead, positive look-behind, non-capturing group, or non-backtracking expression
                        {
                            tokens.Add(recurse(group.Substring(match.Length)));
                        }
                        else if (Regex.IsMatch(group, @"^\?<?!")) // negative look-ahead, or negative look-behind
                        {
                            throw new NotSupportedException("Negative look-arounds are not supported.");
                        }
                        else if (Regex.IsMatch(group, @"^\?<\w+-\w>"))
                        {
                            throw new NotSupportedException("Balancing groups are not supported.");
                        }
                        else if (Regex.IsMatch(group, @"^\?\(.*?(?<!\\)\)"))
                        {
                            throw new NotSupportedException("Conditional groups are not supported.");
                        }
                        else // numbered capture
                        {
                            int index = argc++;
                            if (index >= args.Count)
                            {
                                args.Add(null); // reserve index for later
                                args[index] = recurse(group);
                            }
                            tokens.Add(args[index].ToString());
                        }
                    }
                    else if (pat[i] == '[')
                    {
                        int j = pat.IndexOfUnescaped(']', ++i);
                        string group = pat.Slice(i, j);
                        tokens.Add(group[0] == '^' || group[0] == '\\'
                            ? CharacterClassSelector(ExpandCharacterSet(group).ToArray()).ToString()
                            : group[0].ToString());
                        i = j;
                    }
                    else if (pat[i] == '?')
                    {
                        tokens.Add(tokens.Pop().Repeat(_R.Next(0, 2)));
                        if (pat.Get(i + 1) == '?') ++i;
                    }
                    else if (pat[i] == '*')
                    {
                        tokens.Add(tokens.Pop().Repeat(_R.Next(0, MaxQuantity + 1)));
                        if (pat.Get(i + 1) == '?') ++i;
                    }
                    else if (pat[i] == '+')
                    {
                        tokens.Add(tokens.Pop().Repeat(_R.Next(1, MaxQuantity + 1)));
                        if (pat.Get(i + 1) == '?') ++i;
                    }
                    else if (pat[i] == '{')
                    {
                        int j = pat.IndexOfUnescaped('}', i + 1);
                        string group = pat.Slice(i + 1, j);
                        var match = Regex.Match(group, @"^(\d+)(,(\d+)?)?$");
                        if (j > i && match.Success)
                        {
                            int min = int.Parse(match.Groups[1].Value);
                            int max = match.Groups[2].Success
                                ? match.Groups[3].Success
                                    ? int.Parse(match.Groups[3].Value)
                                    : Math.Max(min, MaxQuantity)
                                : min;
                            tokens.Add(tokens.Pop().Repeat(_R.Next(min, max + 1)));
                            i = j;
                            if (pat.Get(i + 1) == '?') ++i;
                        }
                        else tokens.Add("{");
                    }
                    else if (pat[i] == '|')
                    {
                        disjuncts.Add(tokens);
                        tokens = new List<string>();
                    }
                }

                disjuncts.Add(tokens);
                return string.Concat(DisjunctSelector(disjuncts));
            };

            return recurse(pattern);
        }

        public IEnumerable<char> ExpandCharacterSet(string set)
        {
            if (set.Length == 0)
                return "";

            var sb = new StringBuilder();
            bool invertSet = false;

            if (set[0] == '[' && set[set.Length - 1] == ']')
                set = set.Substring(1, set.Length - 2);
            if (set[0] == '^')
            {
                invertSet = true;
                set = set.Substring(1);
            }
            set = Regex.Unescape(set);

            foreach (Match m in Regex.Matches(set, ".-.|."))
            {
                if (m.Value.Length == 1)
                    sb.Append(m.Value);
                else
                {
                    if (m.Value[0] > m.Value[2]) throw new ArgumentException(string.Format("Invalid character range ({0} > {1}).", m.Value[0], m.Value[2]));
                    for (char c = m.Value[0]; c <= m.Value[2]; ++c)
                        sb.Append(c);
                }
            }

            if (!invertSet) return sb.ToString();

            var A = new HashSet<char>(AllCharacters);
            var B = new HashSet<char>(sb.ToString());
            A.ExceptWith(B);
            return A;
        }
    }
}
